這篇文章我們會專心在處理 Core Data 的部分,會獨立一篇文章處理的原因,是因為我們更改了 QuickRecord 這個 Entity 的部分欄位,造成 Core Data 沒辦法自動幫我們做 Light Weight Migration,所以我們必須協助 Core Data 做新舊資料轉換。
Transaction 為收支記錄使用的資料表,其內容除了 QuickRecord 的部分欄位(金額、標籤等等),還有一些自己的欄位(地點、消費類型等等),因此我們可以新增一個基礎的 QuickRecordBase 物件,用來定義兩者通用的欄位,然後 QuickRecord 和 Transaction 都繼承 QuickRecordBase,如下:
class QuickRecordBase: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var amount: NSDecimalNumber
@NSManaged var audioUUID: UUID
@NSManaged var createdAt: Date
@NSManaged var tags: Set<String>
}
class QuickRecord: QuickRecordBase {
func fetchRequest() -> NSFetchRequest<QuickRecord> {
return NSFetchRequest<QuickRecord>(entityName: "QuickRecord")
}
@NSManaged var isProcessed: Bool
@NSManaged var transaction: Transaction
}
@objc class TransactionType: NSObject, NSCoding {
static let INCOME: TransactionType = TransactionType(rawValue: 0)
static let EXPENSE: TransactionType = TransactionType(rawValue: 1)
let rawValue: Int
private init(rawValue: Int) {
self.rawValue = rawValue
}
func encode(with aCoder: NSCoder) {
aCoder.encode(rawValue, forKey: "rawValue")
}
required init?(coder aDecoder: NSCoder) {
self.rawValue = aDecoder.decodeInteger(forKey: "rawValue")
}
}
class Transaction: QuickRecordBase {
func fetchRequest() -> NSFetchRequest<Transaction> {
return NSFetchRequest<Transaction>(entityName: "Transaction")
}
@NSManaged var location: String
@NSManaged var type: TransactionType
@NSManaged var quickRecord: QuickRecord
}
因為我想在 Transaction 的 type 欄位存 enum,所以我用物件偽裝成 enum 的運作模式,企圖欺騙 Core Data,當然有了 NSCoding 加上 Transformable 的協助,Core Data 根本不是對手。
備註:這裡把 amount 從字串改成 Decimal 的原因,是因為我想偷渡這些 Issue、Issue。
如上修改完後,因為 Core Data 沒辦法瞭解我們這次更動的欄位內容,它不知道如何去做新舊表的 Migration(底層還是 SQLite,沒有黑魔法),所以只要打開 APP 就會直接崩潰,因此我們需要進一步提供 Core Data 有關於我們這次資料表 Migration 的資料。
會看到崩潰訊息中,原因如下:
reason=Can't find or automatically infer mapping model for migration
就是 Core Data 跟你說你改了結構,Core Data 也沒辦法猜測你的目的,所以選擇讓程式崩潰,你得定義出新舊結構轉換的規則。
$ tree Money\ Mom/Model/Model.xcdatamodeld/
Money\ Mom/Model/Model.xcdatamodeld/
├── Model\ 2.xcdatamodel
└── Model.xcdatamodel
2 directories, 0 files
此時系統中就會同時存在 Model 和 Model 2 兩種結構,我們可以在 Xcode 中指定當前版本為 Model 2,也就是第二版的 Model。
指定 Model 2 後,只要 Core Data 開始運作時,它就會發現世界不一樣了,然後開始尋找系統中是否有參考資料,可以協助它做新舊結構轉換,這個參考資料就是「Mapping Model」。
因此我們需新增一個 Mapping Model,然後指定一個 NSEntityMigrationPolicy(如果只是簡單的轉換不一定要用 Policy),該 Policy 中就會定義如何從「舊的 Model」轉換成「新的 Model」,如下:
final class StringAmountToDecimalAmountPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
let dInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
dInstance.setValue(sInstance.value(forKey: "id"), forKey: "id")
dInstance.setValue(NSDecimalNumber(string: sInstance.value(forKey: "amount") as! String), forKey: "amount")
dInstance.setValue(sInstance.value(forKey: "audioUUID"), forKey: "audioUUID")
dInstance.setValue(sInstance.value(forKey: "created_at"), forKey: "createdAt")
dInstance.setValue(sInstance.value(forKey: "tags"), forKey: "tags")
dInstance.setValue(false, forKey: "isProcessed")
dInstance.setValue(nil, forKey: "transaction")
manager.associate(sourceInstance: sInstance, withDestinationInstance: dInstance, for: mapping)
}
}
轉換的程式中,只要有任何地方有錯誤崩潰,轉換就會失敗,Core Data 會將資料還原成轉換前的樣子,所以我們應該放心且適當地使用「!」。
你以為有 GIF 可以看嗎?抱歉,沒有。
如何驗證轉換成功?打開 APP 發現原本資料還在就對了,因為轉換「失敗」只會有兩種結果:
痛苦結束了,下一篇就可以繼續往前實做功能囉。